﻿
// **************************************************************************
//    Class Created by Mick Doherty (Dotnetrix) April 2010
//    http://dotnetrix.co.uk/
//
//    ...for GlassUI (an AeroGlass UI Library)
//    http://glassui.codeplex.com/
// **************************************************************************

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Windows.Forms;
using GlassUI.Design;

namespace GlassUI
{
    [ToolboxBitmap(typeof(TextBox))]
    public class GlassTextBox : TextBox
    {
        public GlassTextBox()
            : base()
        {
            this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            this.SetStyle(ControlStyles.UserPaint, true);
            this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            this.SetStyle(ControlStyles.Opaque, false);
        }

        #region Private Instance Variables

        private Color hiliteColor = SystemColors.Highlight;
        private bool defaultToGlass = true;

        private NativeTextBoxOwner nativeTextBox;

        #endregion

        #region Extended Properties

        [Category("Behavior")]
        [DefaultValue(true)]
        [GlassUIDescription("DefaultToGlassDescription")]
        public bool DefaultToGlass
        {
            get { return defaultToGlass; }
            set { defaultToGlass = value; }
        }

        [DefaultValue(typeof(Color), "Highlight")]
        [Description("The Color used behind SelectedText.")]
        public Color HighlightColor
        {
            get { return hiliteColor; }
            set
            {
                if (!hiliteColor.Equals(value))
                {
                    hiliteColor = value;
                    Invalidate();
                }
            }
        }

        private Rectangle FormatRectangle
        {
            [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
            get
            {
                IntPtr lParam = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(NativeMethods.USER32.RECT)));
                NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.EditControlMessage.EM_GETRECT, IntPtr.Zero, lParam);
                NativeMethods.USER32.RECT rc = (NativeMethods.USER32.RECT)Marshal.PtrToStructure(lParam, typeof(NativeMethods.USER32.RECT));
                Marshal.FreeHGlobal(lParam);
                return rc.ToRectangle();
            }
        }

        #endregion

        #region Overridden properties

        [DefaultValue(typeof(Color),"Window")]
        public override System.Drawing.Color BackColor
        {
            get
            {
                GlassForm form = this.FindForm() as GlassForm;
                if (form != null)
                {
                    if (DefaultToGlass && form.ControlIsOnGlass(this))
                        return Color.Transparent;
                }
                return base.BackColor;
            }
            set
            {
                base.BackColor = value;
            }
        }

        #endregion

        #region Overridden Methods

        protected override void OnCreateControl()
        {
            base.OnCreateControl();
            this.OnFontChanged(EventArgs.Empty);
            // Set Margins to allow for Glow effect.
            NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.EditControlMessage.EM_SETMARGINS, (IntPtr)0x3, GlassUtilities.MAKELPARAM(4, 4));
        }

        protected override void OnFontChanged(EventArgs e)
        {
            // Calling Base.OnFontChanged causes an infinite loop
            // when using AutoComplete* properties.
            //base.OnFontChanged(e);
            IntPtr hFont = this.Font.ToHfont();
            NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.WindowMessage.WM_SETFONT, hFont,(IntPtr)(-1));
            NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.WindowMessage.WM_FONTCHANGE, IntPtr.Zero, IntPtr.Zero);
            this.UpdateStyles();
        }

        // Overridden to implement Ctrl+A and allow standard commands to work when
        // the textbox has focus and standard shortcuts are enabled elsewhere.
        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            if (this.ShortcutsEnabled)
            {
                switch (keyData)
                {
                    case Keys.Control | Keys.A: // Select All
                        this.SelectAll();
                        return true;
                    case Keys.Control | Keys.C: // Copy
                        this.Copy();
                        return true;
                    case Keys.Control | Keys.V: // Paste
                        this.Paste();
                        return true;
                    case Keys.Control | Keys.X: // Cut
                        this.Cut();
                        return true;
                    case Keys.Control | Keys.Z: // Undo
                        if (this.CanUndo)
                            this.Undo();
                        return true;
                    default:
                        break;
                }

            }
            return base.ProcessCmdKey(ref msg, keyData);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            if (this.Text.Length > 0)
            {
                TextFormatFlags format = TextFormatFlags.ExternalLeading | TextFormatFlags.NoPrefix | TextFormatFlags.TextBoxControl;

                if (this.AcceptsTab)
                    format |= TextFormatFlags.ExpandTabs;

                bool rtl = this.RightToLeft == RightToLeft.Yes;

                if (rtl)
                    format |= TextFormatFlags.RightToLeft;

                switch (this.TextAlign)
                {
                    case HorizontalAlignment.Center:
                        format |= TextFormatFlags.HorizontalCenter;
                        break;
                    case HorizontalAlignment.Right:
                        format |= rtl ? TextFormatFlags.Left : TextFormatFlags.Right;
                        break;
                    default:
                        format |= rtl ? TextFormatFlags.Right : TextFormatFlags.Left;
                        break;
                }

                if (!this.Multiline)
                    format |= TextFormatFlags.VerticalCenter;
                
                bool pass = (this.PasswordChar != (char)0) && !this.Multiline;

                if (this.DesignMode)
                {
                    Rectangle rc = new Rectangle(0,-2, this.Width,this.ClientSize.Height+2);
                    GlassUtilities.DrawThemedText(e.Graphics, this.Text, this.Font, rc, this.ForeColor, format, 8);
                }
                else
                {
                    if (this.SelectedText.Length > 0)
                    {
                        bool hilite = this.HideSelection ? this.Focused : true;
                        if (hilite)
                        {
                            using (Brush brush = new SolidBrush(HighlightColor))
                            {
                                foreach (Rectangle rect in SelectedTextRectangles())
                                {
                                    Rectangle r = rect;
                                    if (!Multiline)
                                        r.Offset(0, this.BorderStyle == BorderStyle.None ? 0 : 2);
                                    if (pass)
                                    {
                                        int rcWidth = (int)TextRenderer.MeasureText(e.Graphics, new string(this.PasswordChar, this.SelectionLength), this.Font, this.Size, format | TextFormatFlags.NoPadding).Width;
                                        r = new Rectangle(rect.X, rect.Y, rcWidth, rect.Height);
                                    }
                                    e.Graphics.FillRectangle(brush, r);
                                }
                            }
                        }
                    }

                    Color textColor = this.Enabled ? this.ForeColor : SystemColors.GrayText;

                    IEnumerator lines = this.DisplayedLines.GetEnumerator();
                    while (lines.MoveNext())
                    {
                        Rectangle fr = this.FormatRectangle;
                        DictionaryEntry entry = ((DictionaryEntry)lines.Current);
                        Rectangle rc = LineSelectionBounds((int)entry.Key, 0, this.Text.Length);
                        if (rc.Top >= fr.Top && rc.Bottom <= fr.Bottom || !this.Multiline)
                        {
                            rc.Inflate(4, 4);
                            string txt = (string)entry.Value;

                            if (!Multiline)
                                rc.Offset(0, this.BorderStyle == BorderStyle.None ? 0 : 2);

                            if (pass)
                            {
                                txt = new string(PasswordChar, txt.Length);
                                rc.Width = this.Width;
                            }
                            GlassUtilities.DrawThemedText(e.Graphics, txt, this.Font, rc, textColor, format, 8);
                        }
                    }

                }
            }
        }

        protected override void OnResize(EventArgs e)
        {
            base.OnResize(e);
            UpdateDisplayedLines();
        }

        protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);
            nativeTextBox = new NativeTextBoxOwner(this);
            AssignNativeTextBox();
        }

        protected override void OnParentChanged(EventArgs e)
        {
            base.OnParentChanged(e);
            AssignNativeTextBox();
            this.OnFontChanged(EventArgs.Empty);
            this.UpdateDisplayedLines();
        }

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        protected override void WndProc(ref Message m)
        {
            if (m.Msg == (int)NativeMethods.USER32.WindowMessage.WM_ERASEBKGND && this.BackColor == Color.Transparent)
                return;

            base.WndProc(ref m);

            if (m.Msg == (int)(NativeMethods.USER32.WindowMessage.WM_REFLECT | NativeMethods.USER32.WindowMessage.WM_COMMAND))
            {
                int code = GlassUtilities.HIWORD(m.WParam);
                if (Enum.IsDefined(typeof(NativeMethods.USER32.EditControlNotificationCode), code))
                {
                    switch (code)
                    {
                        case (int)NativeMethods.USER32.EditControlNotificationCode.EN_CHANGE:
                        case (int)NativeMethods.USER32.EditControlNotificationCode.EN_UPDATE:
                        case (int)NativeMethods.USER32.EditControlNotificationCode.EN_VSCROLL:
                        case (int)NativeMethods.USER32.EditControlNotificationCode.EN_HSCROLL:
                        case (int)NativeMethods.USER32.EditControlNotificationCode.EN_SETFOCUS:
                            this.UpdateDisplayedLines();
                            this.Invalidate();
                            break;
                        default:
                            //Console.WriteLine(Enum.GetName(typeof(EditControlNotificationCode), code));
                            break;
                    }
                }
            }
        }

        #endregion

        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        private void AssignNativeTextBox()
        {
            if (Parent != null && nativeTextBox != null)
            {
                if (nativeTextBox.Handle != null)
                    nativeTextBox.ReleaseHandle();
                nativeTextBox.AssignHandle(Parent.Handle);
            }
        }

        private List<Rectangle> SelectedTextRectangles()
        {
            int firstCharIndex = this.SelectionStart;
            int lastCharIndex = this.SelectionStart + this.SelectionLength;

            int startLine = this.GetLineFromCharIndex(firstCharIndex);
            int endLine = this.GetLineFromCharIndex(lastCharIndex);

            List<Rectangle> rc = new List<Rectangle>();

            for (int id = startLine; id <= endLine; id++)
            {
                Rectangle fr = this.FormatRectangle;
                Rectangle r = this.LineSelectionBounds(id, firstCharIndex, lastCharIndex);
                if (r.Top >= fr.Top && r.Bottom <= fr.Bottom || !this.Multiline)
                    rc.Add(r);
            }

            return rc;
        }

        private Rectangle LineSelectionBounds(int lineNumber, int firstCharIndex, int lastCharIndex)
        {
            int firstChar = Math.Max(firstCharIndex, this.GetFirstCharacterOfLine(lineNumber));
            int lastChar = Math.Min(lastCharIndex, this.GetLastCharacterOfLine(lineNumber));

            string text = this.Text.Substring(firstChar, lastChar - firstChar);

            Point pt = this.GetPositionFromCharIndex(firstChar);

            Size sz;

            TextFormatFlags format = TextFormatFlags.ExternalLeading | TextFormatFlags.NoPrefix | TextFormatFlags.TextBoxControl | TextFormatFlags.NoPadding;

            if (this.AcceptsTab)
                format |= TextFormatFlags.ExpandTabs;

            bool rtl = this.RightToLeft == RightToLeft.Yes;

            if (rtl)
                format |= TextFormatFlags.RightToLeft;

            switch (this.TextAlign)
            {
                case HorizontalAlignment.Center:
                    format |= TextFormatFlags.HorizontalCenter;
                    break;
                case HorizontalAlignment.Right:
                    format |= rtl ? TextFormatFlags.Left : TextFormatFlags.Right;
                    break;
                default:
                    format |= rtl ? TextFormatFlags.Right : TextFormatFlags.Left;
                    break;
            }

            using (Graphics g = this.CreateGraphics())
                sz = TextRenderer.MeasureText(g, text, this.Font, Size.Empty, format);

            return new Rectangle(pt, sz);
        }

        #region Native Method Calls

        public override Point GetPositionFromCharIndex(int index)
        {
            IntPtr val = NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.EditControlMessage.EM_POSFROMCHAR, (IntPtr)index, IntPtr.Zero);
            return new Point(val.ToInt32());
        }

        public override int GetLineFromCharIndex(int index)
        {
            IntPtr val = NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.EditControlMessage.EM_LINEFROMCHAR, (IntPtr)index, IntPtr.Zero);
            return val.ToInt32();
        }

        public int GetFirstCharacterOfLine(int lineNumber)
        {
            return NativeMethods.USER32.SendMessage(this.Handle,
                          NativeMethods.USER32.EditControlMessage.EM_LINEINDEX,
                          (IntPtr)lineNumber, IntPtr.Zero).ToInt32();
        }

        public int GetLastCharacterOfLine(int lineNumber)
        {
            int firstChar = this.GetFirstCharacterOfLine(lineNumber);
            return this.GetLineLength(firstChar) + firstChar;
        }

        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public int GetFirstVisibleLine()
        {
            return NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.EditControlMessage.EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero).ToInt32();
        }

        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public int GetLastVisibleLine()
        {
            int result = this.GetCharIndexFromPosition(new Point(0, this.FormatRectangle.Bottom - this.Font.Height));

            return this.GetLineFromCharIndex(result);
        }

        public int GetLineLength(int characterIndex)
        {
           return NativeMethods.USER32.SendMessage(this.Handle, 
               NativeMethods.USER32.EditControlMessage.EM_LINELENGTH, 
               (IntPtr)characterIndex, IntPtr.Zero).ToInt32();
        }

        // This simple method caused me real problems.
        [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
        public string GetLine(int index)
        {
            int lineLength = this.GetLineLength(GetFirstCharacterOfLine(index));

            IntPtr ptr = Marshal.AllocHGlobal(lineLength);

            if (lineLength > 1)
            {
                // ... and it was these 2 lines of code that I was missing.
                // pay more attention to the MSDN Documentation, it was explained :D
                byte[] textSize = BitConverter.GetBytes((short)lineLength);
                Marshal.Copy(textSize, 0, ptr, 2);
                // ...although I found no mention that it would return incorrect
                // results if the line was only 1 character long.
            }

            NativeMethods.USER32.SendMessage(this.Handle, NativeMethods.USER32.EditControlMessage.EM_GETLINE, (IntPtr)index, ptr);

            String line = Marshal.PtrToStringAnsi(ptr, lineLength);

            Marshal.FreeHGlobal(ptr);

            return line;

        }

        #endregion

        private Hashtable displayedLines = new Hashtable();
        internal Hashtable DisplayedLines
        {
            get { return displayedLines; }
            //set
            //{
            //    if (displayedLines != value)
            //    {
            //        displayedLines = value;
            //        this.Invalidate();
            //    }
            //}
        }

        internal void UpdateDisplayedLines()
        {
            if (this.IsHandleCreated && !this.IsDisposed)
            {
                displayedLines.Clear();
                if (this.Multiline)
                {
                    for (int id = this.GetFirstVisibleLine(); id <= this.GetLastVisibleLine(); id++)
                    {
                        displayedLines.Add(id, this.GetLine(id));
                    }
                }
                else
                    displayedLines.Add(0, this.Text);
            }
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        [SecurityPermission(SecurityAction.InheritanceDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        internal class NativeTextBoxOwner : NativeWindow
        {
            private static IntPtr hollowBrush = NativeMethods.GDI32.GetStockObject(NativeMethods.GDI32.HOLLOW_BRUSH);

            public NativeTextBoxOwner(TextBox textBox)
                : base()
            {
                this.txtBox = textBox;
            }

            private TextBox txtBox;

            [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
            protected override void WndProc(ref Message m)
            {
                switch (m.Msg)
                {
                    case (int)NativeMethods.USER32.WindowMessage.WM_CTLCOLOREDIT:
                    case (int)NativeMethods.USER32.WindowMessage.WM_CTLCOLORSTATIC:
                        if (m.LParam != txtBox.Handle)
                        {
                            DefWndProc(ref m);
                        }
                        else
                        {
                            int result = NativeMethods.GDI32.SetBkMode(m.WParam, NativeMethods.GDI32.TRANSPARENT);
                            if (result != 0)
                            {
                                m.Result = hollowBrush;
                            }
                            if (!this.txtBox.Multiline)
                                this.txtBox.Invalidate();
                        }
                        break;
                }
                base.WndProc(ref m);
            }
        }

    }

}